3

上一篇文章介绍了clipboard.js这个工具库中的第一个依赖select这个工具库主要完成了对任意DOM元素的复制到粘贴板的功能。这次介绍一下clipboard.js源码中的第二个依赖的轻型工具库tiny-emitter这个工具库主要用来实现一个简易的基于监听发布者模式的事件派发和接收器,代码经过我的es6改写后只有40行,没有依赖第三方库,实现的功能却是比较强大的,而且可以根据实际情况方便的进行扩展。

快速上手

在研究源码之前,先看一下最普遍的使用场景。

const Emitter = require('./emitter')

let emitter = new Emitter()

// on 一个事件

let sayHello = name => console.log(`hello, ${name}`)
emitter.on('helloName', sayHello)
// emit 一个事件

// emitter.emit('helloName', 'dongzhe')

// on一个带有作用域的同一个事件
let obj = {
    prefix: 'smith',
    thankName (name) {
        console.log(`hello, ${this.prefix}.${name}`)
        return `hello, ${this.prefix}.${name}`
    }
}

emitter.on('helloName', obj.thankName, obj)
emitter.emit('helloName', 'dongzhe')

// new other emitter 可以在这里分组 不同的组可以有同样的eventName
let emitter1 = new Emitter()

let sayHaHa = name => console.log(`haha, ${name}`)
emitter1.on('helloName', sayHaHa)
// emit 一个事件

emitter1.emit('helloName', 'dongzhe')

可以看出,每一个事件管理器都是一个对象,可以根据不同的业务场景模块创建不同的事件管理器,事件管理器最基本功能就是动态的订阅事件和派发事件,当然还可以取消事件。用于在同一主模块下的不同子模块以及不同主模块之间的通信,支持动态绑定作用域。如果用过vue的父子组件事件通信以及eventBus,对事件管理器应该不会陌生的。

源码实现

事件管理模型主要由4个函数构成,

  • on 用于订阅事件,一个事件订阅多个触发函数
  • emit 用于发布事件,发布时会以此触发事件订阅的函数
  • once 订阅的事件只触发一次
  • off 取消订阅事件,支持指定取消,批量取消和全部取消

代码结构

class E {
    constructor () {
        this.eventObj = {}
    }
    on () {}
    once () {}
    emit () {}
    off () {}
}

module.exports = E

Emitter对象存在一个事件对象,以键值对的形式保存事件名称和对应的触发事件。

订阅事件 on

订阅事件就是把要触发的函数放到事件对应的对象里面,如果事件不存在,需要初始化一下即可。一个事件可以动态的订阅多个触发函数。而且支持指定作用域,可以远程调用任意模块的函数。

on (eventName, callback, ctx) {
    // 一个eventName可以绑定多个事件
    (this.eventObj[eventName] || (this.eventObj[eventName] = [])).push({callback, ctx})
    return this
}

发布事件 emit

相对订阅事件的就是发布事件,发布事件接收事件的事件名和触发函数的参数,将对应事件订阅的触发函数依次执行即可,参数可以使用es6rest操作符。

emit (eventName, ...args) {
    let eventArr = (this.eventObj[eventName] || []).slice()
    eventArr.forEach(ele => ele.callback.call(ele.ctx, args))
    return this
}

取消事件 off

相对订阅事件,也应该可以取消事件,取消事件可以有多种选择,可以指定取消事件订阅的某一个或者多个触发函数,也可以直接将整个事件都取消掉。取消事件接收取消的事件名称,和一个可选的函数对象或者函数对象数组(我自己增加的),如果传入了指定的触发函数对象,通过遍历所有触发的函数来过滤掉需要取消的触发函数,最后重新赋值即可。如果没有传触发函数,那么就认为取消整个订阅的事件,直接从全局的事件对象中删除订阅对象即可

off (eventName, callback) {
    if (Object.prototype.toString.call(callback) === "[object Array]") {
        callback.forEach(func => this.off(eventName, func))
        return this
    } 
    let liveEvents = []
    let obj = this.eventObj
    let eventArr = obj[eventName]
    // 如果没有callback 就删除掉整个eventName对象
    if (eventArr && callback) {
        liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))
    }
    (liveEvents.length) ? obj[eventName] = liveEvents : delete obj[eventName]
    return this
}

其中最主要的就是下面这一行代码了,使用filter过滤掉需要取消的触发函数,ele.callback._ !== callback是为了兼容once后面马上就说到。

liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))

一次触发 once

有的时候我们只需要触发一次订阅的事件,比如用户刚登录进来获取历史消息或者通知消息,触发一次后就不需要了,所以有了once函数,once函数主要的工作原理就是,在函数内部添加一个代理函数listener代理函数用来为触发函数做代理,做代理的目的是为了添加逻辑,这个逻辑就是在触发函数第一次执行的时候,就自动执行off函数,用来取消触发函数的逻辑。

let listener = (...args) => {
    this.off(eventName, listener)
    callback.apply(ctx, args)
}
// 因为listener是在callback上封装了一层 所以要规定一个可以找到callbak的规则
listener._ = callback

因为listener是在callback上封装了一层代理 所以要规定一个可以找到callback的规则,这样off函数在传入取消函数的时候,我们可以顺利的用兼容的方式找到。
最后其实订阅的是这个代理函数listener

once (eventName, callback, ctx) {
    let listener = (...args) => {
        this.off(eventName, listener)
        callback.apply(ctx, args)
    }
    // 因为listener是在callback上封装了一层 所以要规定一个可以找到callbak的规则
    listener._ = callback
    return this.on(eventName, listener, ctx)
}

完整代码

我自己在原来的代码基础上用es6重新编写,并添加了一些逻辑,可以对比原来的代码来看,最后完整的代码如下

class E {
    constructor () {
        this.eventObj = {}
    }
    on (eventName, callback, ctx) {
        // 一个eventName可以绑定多个事件
        (this.eventObj[eventName] || (this.eventObj[eventName] = [])).push({callback, ctx})
        return this
    }
    once (eventName, callback, ctx) {
        let listener = (...args) => {
            this.off(eventName, listener)
            callback.apply(ctx, args)
        }
        // 因为listener是在callback上封装了一层 所以要规定一个可以找到callbak的规则
        listener._ = callback
        return this.on(eventName, listener, ctx)
    }
    emit (eventName, ...args) {
        let eventArr = (this.eventObj[eventName] || []).slice()
        eventArr.forEach(ele => ele.callback.call(ele.ctx, args))
        return this
    }
    off (eventName, callback) {
        if (Object.prototype.toString.call(callback) === "[object Array]") {
            callback.forEach(func => this.off(eventName, func))
            return this
        } 
        let liveEvents = []
        let obj = this.eventObj
        let eventArr = obj[eventName]
        // 如果没有callback 就删除掉整个eventName对象
        if (eventArr && callback) {
            liveEvents = eventArr.filter(ele => (ele.callback !== callback && ele.callback._ !== callback))
        }
        (liveEvents.length) ? obj[eventName] = liveEvents : delete obj[eventName]
        return this
    }
}

module.exports = E

结语

这只是一个比较简单的事件订阅发布器,但包含的核心思想还是比较完整的,用到了面向对象,订阅发布者模式,代理模式等,而且可以根据自己的需求进行很方便的扩展,比如我扩展的批量取消,也可以添加批量订阅,甚至使用promise来封装异步触发,每一个函数都返回了对象本身,可以完成链式调用,比如订阅完成后立刻触发完成初始化等等。


dongzhe3917875
1.5k 声望76 粉丝